
/*
 * Copyright (c) 2010 Sergey N Frolov
 * 
 * See LICENSE file
 */

#include "ngx_javascript.h"

size_t ngx_javascript_strlcpy(char *dst, const char *src, size_t siz) {
	char *d = dst;
	const char *s = src;
	size_t n = siz;
	if (n != 0) {
		while (--n != 0) {
			if ((*d++ = *s++) == '\0')
				break;
		}
	}
	if (n == 0) {
		if (siz != 0)
			*d = '\0';
		while (*s++)
			;
	}
	return(s - src - 1);
}

size_t ngx_javascript_strlcat(char *dst, const char *src, size_t siz) {
	char *d = dst;
	const char *s = src;
	size_t n = siz;
	size_t dlen;
	while (n-- != 0 && *d != '\0')
		d++;
	dlen = d - dst;
	n = siz - dlen;
	if (n == 0)
		return(dlen + strlen(s));
	while (*s != '\0') {
		if (n != 1) {
			*d++ = *s;
			n--;
		}
		s++;
	}
	*d = '\0';
	return(dlen + (s - src));
}

char *ngx_javascript_realpath(ngx_javascript_worker_t *worker, const char *path, char resolved[PATH_MAX]) {
	struct stat sb;
	char *p, *q, *s;
	size_t left_len, resolved_len;
	unsigned symlinks;
	int serrno, slen;
	char left[PATH_MAX], next_token[PATH_MAX], symlink[PATH_MAX];
	serrno = errno;
	symlinks = 0;
	if (path[0] == '/') {
		resolved[0] = '/';
		resolved[1] = '\0';
		if (path[1] == '\0')
			return (resolved);
		resolved_len = 1;
		left_len = ngx_javascript_strlcpy(left, path + 1, sizeof(left));
	} else {
		resolved[0] = '\0';
		if (worker == NULL || worker->client == NULL || worker->client->document_root == NULL) {
			ngx_javascript_strlcpy(resolved, ".", PATH_MAX);
			return (NULL);
		}
		resolved_len = ngx_javascript_strlcat(resolved, worker->client->document_root, PATH_MAX);
		left_len = ngx_javascript_strlcpy(left, path, sizeof(left));
	}
	if (left_len >= sizeof(left) || resolved_len >= PATH_MAX) {
		errno = ENAMETOOLONG;
		return (NULL);
	}
	while (left_len != 0) {
		p = strchr(left, '/');
		s = p ? p : left + left_len;
		if (s - left >= sizeof(next_token)) {
			errno = ENAMETOOLONG;
			return (NULL);
		}
		memcpy(next_token, left, s - left);
		next_token[s - left] = '\0';
		left_len -= s - left;
		if (p != NULL)
			memmove(left, s + 1, left_len + 1);
		if (resolved[resolved_len - 1] != '/') {
			if (resolved_len + 1 >= PATH_MAX) {
				errno = ENAMETOOLONG;
				return (NULL);
			}
			resolved[resolved_len++] = '/';
			resolved[resolved_len] = '\0';
		}
		if (next_token[0] == '\0')
			continue;
		else if (strcmp(next_token, ".") == 0)
			continue;
		else if (strcmp(next_token, "..") == 0) {
			if (resolved_len > 1) {
				resolved[resolved_len - 1] = '\0';
				q = strrchr(resolved, '/') + 1;
				*q = '\0';
				resolved_len = q - resolved;
			}
			continue;
		}
		resolved_len = ngx_javascript_strlcat(resolved, next_token, PATH_MAX);
		if (resolved_len >= PATH_MAX) {
			errno = ENAMETOOLONG;
			return (NULL);
		}
		if (ngx_javascript_lstat(worker, resolved, &sb) != 0) {
			if (errno == ENOENT && p == NULL) {
				errno = serrno;
				return (resolved);
			}
			return (NULL);
		}
		if (S_ISLNK(sb.st_mode)) {
			if (symlinks++ > MAXSYMLINKS) {
				errno = ELOOP;
				return (NULL);
			}
			slen = ngx_javascript_readlink(worker, resolved, symlink, sizeof(symlink) - 1);
			if (slen < 0)
				return (NULL);
			symlink[slen] = '\0';
			if (symlink[0] == '/') {
				resolved[1] = 0;
				resolved_len = 1;
			} else if (resolved_len > 1) {
				resolved[resolved_len - 1] = '\0';
				q = strrchr(resolved, '/') + 1;
				*q = '\0';
				resolved_len = q - resolved;
			}
			if (p != NULL) {
				if (symlink[slen - 1] != '/') {
					if (slen + 1 >= sizeof(symlink)) {
						errno = ENAMETOOLONG;
						return (NULL);
					}
					symlink[slen] = '/';
					symlink[slen + 1] = 0;
				}
				left_len = ngx_javascript_strlcat(symlink, left, sizeof(left));
				if (left_len >= sizeof(left)) {
					errno = ENAMETOOLONG;
					return (NULL);
				}
			}
			left_len = ngx_javascript_strlcpy(left, symlink, sizeof(left));
		}
	}
	if (resolved_len > 1 && resolved[resolved_len - 1] == '/')
		resolved[resolved_len - 1] = '\0';
	return (resolved);
}

int ngx_javascript_lstat(ngx_javascript_worker_t *worker, const char *path, struct stat *buf) {
# if 0
	return lstat(path, buf);
# else
	if (worker->stat_cache == NULL) {
		worker->stat_cache = JS_NewHashTable(0, JS_HashString, ngx_javascript_hash_strcmp, JS_CompareValues, NULL, NULL);
	}
	ngx_javascript_stat_t *jst = JS_HashTableLookup(worker->stat_cache, path);
	if (jst == NULL) {
		if (worker->stat_have == 0) {
			worker->stat_data = (ngx_javascript_stat_t *) ngx_javascript_worker_allocate(worker, 10 * sizeof(ngx_javascript_stat_t));
			if (worker->stat_data == NULL) return lstat(path, buf);
			worker->stat_have = 10;
		}
		jst = worker->stat_data;
		worker->stat_data ++;
		worker->stat_have --;
		JS_HashTableAdd(worker->stat_cache, path, jst);
		if (lstat(path, buf) == 0) {
			jst->no = 0;
			memcpy(&jst->st, buf, sizeof(struct stat));
			return 0;
		} else {
			jst->no = errno;
			return -1;
		}
	} else {
		if (jst->no == 0) {
			memcpy(buf, &jst->st, sizeof(struct stat));
			return 0;
		} else {
			errno = jst->no;
			return -1;
		}
	}
	return 0;
# endif
}

int ngx_javascript_readlink(ngx_javascript_worker_t *worker, const char *path, char *buf, size_t bufsize) {
# if 0
	return readlink(path, buf, bufsize);
# else
	int ok, *no, length;
	char *rel = NULL;
	if (worker->link_cache == NULL) {
		worker->link_cache = JS_NewHashTable(0, JS_HashString, ngx_javascript_hash_strcmp, JS_CompareValues, NULL, NULL);
	} else {
		rel = JS_HashTableLookup(worker->link_cache, path);
	}
	if (rel == NULL) {
		ok = readlink(path, buf, bufsize);
		length = sizeof(int) + (ok < 0 ? 0 : ok) + 1;
		if (worker->link_have < length) {
			worker->link_data = ngx_javascript_worker_allocate(worker, worker->server->pagesize);
			if (worker->link_data == NULL) return ok;
			worker->link_have = worker->server->pagesize;
		}
		rel = worker->link_data;
		worker->link_data += length;
		worker->link_have -= length;
		JS_HashTableAdd(worker->link_cache, path, rel);
		no = (int *) rel;
		*no = ok < 0 ? errno : 0;
		memcpy(rel + sizeof(int), buf, ok < 0 ? 0 : ok);
		rel[sizeof(int) + (ok < 0 ? 0 : ok)] = '\0';
	} else {
		no = (int *) rel;
		ok = *no == 0 ? strlen(rel + sizeof(int)) : -1;
		if (ok >= 0) memcpy(buf, rel + sizeof(int), ok);
	}
	return ok;
# endif
}

/*
 * Convert relative path to absolute
 */
JSBool ngx_javascript_fullpath(JSContext *jcx, char *filename, char *fullname) {
	ngx_javascript_client_t *client = JS_GetContextPrivate(jcx);
	if (client == NULL) return ngx_javascript_private_error(jcx);
	fullname[0] = '\0';
	strcat(fullname, (char *) client->document_root);
	strcat(fullname, "/");
	strcat(fullname, filename);
	return JS_TRUE;
}

/*
 * Check filename is in basedir path
 * JS_TRUE if yes, JS_FALSE if no
 * mode is either 0 (file should be accessed read-only) or O_CREAT (create if not exists)
 */
JSBool ngx_javascript_check_basedir(JSContext *jcx, char *filename, int mode) {
	ngx_javascript_client_t *client;
	client = JS_GetContextPrivate(jcx);
	if (client == NULL) return ngx_javascript_private_error(jcx);
	if (client->open_basedir == NULL) {
		return JS_TRUE;
	} else {
		char c = '/';
		struct stat st;
		char *slash = NULL;
		if (ngx_javascript_lstat(client->worker, filename, &st) == -1) {
			if (mode & O_CREAT) {
				slash = filename + strlen(filename) - 1;
				if (slash - filename <= 0) return JS_FALSE;
				while (*slash == '/' && slash > filename) slash --;
				while (*slash != '/' && slash > filename) slash --;
				while (*slash == '/' && slash > filename) slash --;
				slash ++;
				c = *slash;
				*slash = '\0';
				if (ngx_javascript_lstat(client->worker, filename, &st) == -1) {
					*slash = c;
					return JS_FALSE;
				}
			} else {
				return JS_FALSE;
			}
		}
		{
			char realname[PATH_MAX];
			char *realdir;
			size_t i, l, length, reallen;
			if (ngx_javascript_realpath(client->worker, filename, realname) == NULL) {
				if (slash != NULL) *slash = c;
				return JS_FALSE;
			}
			reallen = strlen(realname);
			realdir = (char *) client->open_basedir;
			length = client->open_basedir_len;
			for (i = 0; i < length; i ++) {
				l = strlens(realdir + i);
				if (l == 0) {
					continue;
				} else if (l == 1 && realdir[i] == '~') {
					if (reallen >= client->document_root_len && strncmp(realname, client->document_root, client->document_root_len) == 0 && (realname[client->document_root_len] == '/' || realname[client->document_root_len] == '\0')) {
						if (slash != NULL) *slash = c;
						return JS_TRUE;
					}
				} else if (reallen >= l) {
					if (strncmp(realname, realdir + i, l) == 0 && (realname[l] == '/' || realname[l] == '\0')) {
						if (slash != NULL) *slash = c;
						return JS_TRUE;
					}
				}
				i += l;
			}
			if (slash != NULL) *slash = c;
		}
	}
	return JS_FALSE;
}

/*
 * Check filename is in basedir path. filename is real path
 * JS_TRUE if yes, JS_FALSE if no
 * The same as above but doesn't do realpath() and uses less memory
 * Is used primarily in script processing
 */
JSBool ngx_javascript_check_basedir_realpath(JSContext *jcx, const char *filename) {
	ngx_javascript_client_t *client;
	client = JS_GetContextPrivate(jcx);
	if (client == NULL) return ngx_javascript_private_error(jcx);
	if (client->open_basedir == NULL) {
		return JS_TRUE;
	} else {
		size_t i, l;
		size_t filename_len = strlen(filename);
		char *realdir = client->open_basedir;
		size_t length = client->open_basedir_len;
		for (i = 0; i < length; i ++) {
			l = strlens(realdir + i);
			if (l == 0) {
				continue;
			} else if (l == 1 && realdir[i] == '~') {
				// Check filename is in document root
				if (
					// filename length is at least document root length
					filename_len >= client->document_root_len
					&&
					// filename starts with document root ...
					strncmp(filename, client->document_root, client->document_root_len) == 0
					&&
					// ... and continues with '/' or ends
					(filename[client->document_root_len] == '/' || filename[client->document_root_len] == '\0')
				) {
					return JS_TRUE;
				}
			} else if (filename_len >= l) {
				// Check filename is in realdir
				if (
					// filename starts with realdir ...
					strncmp(filename, realdir + i, l) == 0
					&&
					// ... and continues with '/' or ends
					(filename[l] == '/' || filename[l] == '\0')
				) {
					return JS_TRUE;
				}
			}
			i += l;
		}
	}
	return JS_FALSE;
}

size_t strlens(const char *str) {
	register const char *s;
	for (s = str; *s != '\0' && *s != ':'; ++s);
	return(s - str);
}
